/**
 * BrandBigData.com Inc. Copyright (c) 2018 All Rights Reserved.
 */
package com.qibao.toolkit.dao;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 在多租户环境下更新实体属性，where条件中包含分库分表的key
 *
 * @author wangcong
 * @version $Id: MultiTenantRepositoryImpl.java, v0.1 2018/3/21 14:31 wangcong Exp $$
 */
@Transactional(readOnly = true)
public class UpdateJpaRepositoryImpl<T, ID extends Serializable, Condition extends NameValuePair>
        extends SimpleJpaRepository<T, ID>
        implements UpdateJpaRepository<T, ID, Condition> {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final EntityManager entityManager;


    public UpdateJpaRepositoryImpl(
            JpaEntityInformation<T, ?> entityInformation,
            EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
    }

    public UpdateJpaRepositoryImpl(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        this.entityManager = em;
    }

    @Transactional
    @Override
    public <S extends T> S updateData(S entity, ID id, Condition... conditions) {
        entityManager.clear();
        if (Objects.isNull(id)) {
            throw new RuntimeException("primary key is null");
        }

        SqlStatement statement = buildUpdateSql(entity, id, conditions);
        String sql = statement.getSql();
        logger.debug("update sql: {}, parameters: {}", sql, statement.getValues());

        Query query = entityManager.createQuery(sql);
//        query = setParameters(query, statement.getValues());
        query = setParameters(query, statement.getValues());

        int i = query.executeUpdate();
        if (i == 0) {
            logger.warn("zero of entities updated or deleted");
            throw new RuntimeException("update error");
        } else {
            return entity;
        }
    }


    @Transactional
    @Override
    public <S extends T> S insertData(S entity) {

        entityManager.persist(entity);

        return entity;
    }

    /**
     * 暂未实现
     */
   /* @Transactional
    public <S extends T> S insertDataByNative(S domain) {

        SqlStatement statement = buildInsertSql(domain);
        String sql = statement.getSql();
        logger.info("insert sql: {}, parameters: {}", sql, statement.getValues());

        Query query = entityManager.createQuery(sql);
        query = setParameters(query, statement.getValues());

        query.executeUpdate();

        return domain;
    }*/

    /**
     * 构建 insert sql
     *
     * @param domain 实体
     */
   /* public <S extends T> SqlStatement buildInsertSql(S domain) {
        StringBuilder sb = new StringBuilder("insert into " + domain.getClass().getSimpleName() + " (");
        List<Object> values = new ArrayList<>();

        try {
            for (PropertyDescriptor pd : getPropertyDescriptors(domain.getClass())) {

                String fieldName = pd.getName();

                Method readMethod = pd.getReadMethod();
                Object value = readMethod.invoke(domain, null);

                if ("class".equalsIgnoreCase(fieldName)) {
                    continue;
                }

                if (Objects.isNull(value)) {
                    continue;
                }

                sb.append(fieldName + ", ");

                values.add(value);
            }

            sb.delete(sb.length() - 2, sb.length());

            sb.append(") value (");
            IntStream.rangeClosed(1, values.size()).forEach(i -> sb.append("?, "));

            sb.delete(sb.length() - 2, sb.length());
            sb.append(")");

        } catch (IllegalAccessException | InvocationTargetException e) {
            logger.error("", e);
            throw new RuntimeException(e);
        }

        return new SqlStatement(sb, values);

    }*/


    /**
     * 构建 update sql
     *
     * @param entity 实体
     * @param id 主键
     * @param conditions 条件
     */
    public <S extends T> SqlStatement buildUpdateSql(S entity, ID id, Condition... conditions) {

        StringBuilder sb = new StringBuilder(
                "update " + entity.getClass().getSimpleName() + " set ");
//        List<Object> values = new ArrayList<>();

        Map<String, Object> params = new LinkedHashMap<>();

        try {
            for (PropertyDescriptor pd : getPropertyDescriptors(entity.getClass())) {

                String filedName = pd.getName();

                if ("id".equalsIgnoreCase(filedName)) {
                    Method writeMethod = pd.getWriteMethod();
                    writeMethod.invoke(entity, id);
                    continue;
                } else if ("class".equalsIgnoreCase(filedName)) {
                    continue;
                }

                Method readMethod = pd.getReadMethod();
                Object value = readMethod.invoke(entity, null);
                if (Objects.isNull(value)) {
                    continue;
                }

                sb.append(filedName + " = :" + filedName + ", ");
                params.put(filedName, value);
//                values.add(value);
            }

            if (!params.isEmpty()) {
                sb.delete(sb.length() - 2, sb.length() - 1);

                //append where id
                sb.append(" where id = :id ");
//                values.add(id);
                params.put("id", id);

                // append table key
                if (Objects.nonNull(conditions) && conditions.length > 0) {

                    for (Condition key : conditions) {
                        sb.append(" and " + key.getKey() + " = :" + key.getKey() + " ");
//                        values.add(key.getValue());
                        params.put(String.valueOf(key.getKey()), key.getValue());
                    }

                }
            }

            return new SqlStatement(sb, params);

        } catch (IllegalAccessException | InvocationTargetException e) {
            logger.error("", e);
            throw new RuntimeException(e);
        }
    }


    private Query setParameters(Query query, Map<String, Object> parameters) {

        parameters.forEach((key, val) -> query.setParameter(key, val));

//        for (int i = 0; i < parameters.size(); i++) {
////            query.setParameter(i, parameters.get(i - 1));
////            下标是0开始
//            query.setParameter(i, parameters.get(i));
//        }
        return query;
    }

    private PropertyDescriptor[] getPropertyDescriptors(Class<?> clz) {
        try {
            return Introspector.getBeanInfo(clz).getPropertyDescriptors();
        } catch (IntrospectionException e) {
            logger.error("", e);
            throw new RuntimeException(e);
        }
    }


    public class SqlStatement {

        private StringBuilder sqlBuilder;
        //        private List<Object> values;
        private Map<String, Object> values;

        public SqlStatement() {
            this.sqlBuilder = new StringBuilder();
            this.values = new LinkedHashMap<>();
        }

        public SqlStatement(StringBuilder sqlBuilder, Map<String, Object> values) {
            this.sqlBuilder = sqlBuilder;
            this.values = values;
        }


        public StringBuilder getSqlBuilder() {
            return sqlBuilder;
        }

        public void setSqlBuilder(StringBuilder sqlBuilder) {
            this.sqlBuilder = sqlBuilder;
        }

        public Map<String, Object> getValues() {
            return values;
        }

        public void setValues(Map<String, Object> values) {
            this.values = values;
        }

        public String getSql() {
            return this.sqlBuilder.toString();
        }
    }

}
